<?php
// $Id: image.php 207 2012-03-14 09:52:26Z tomqin@gmail.com $

/**
 * Image 类封装了针对图像的操作
 *
 * 开发者不能直接构造该类的实例，而是应该用 Image::createFromFile()
 * 静态方法创建一个 Image 类的实例。
 *
 * 操作大图片时，请确保 php 能够分配足够的内存。
 *
 * @copyright		Copyright (C) 2010-2012 ARESCMS Inc.
 * @author			TomQin <tomqin@gmail.com>
 * @license			http://www.arescms.cn/license/
 * @package Core
 */

abstract class Image
{
	/**
	 * 从指定文件创建 ImageGD 对象
	 *
	 * 用法：
	 * <code>
	 * <?php
	 * $image = Image::createFromFile('1.jpg');
	 * $image->resize($width, $height);
	 * $image->saveAsJpeg('2.jpg');
	 * ?>
	 * </code>
	 *
	 * 对于上传的文件，由于其临时文件名中并没有包含扩展名。
	 * 因此需要采用下面的方法创建 Image 对象：
	 *
	 * <code>
	 * <?php
	 * $ext = pathinfo($_FILES['upfile']['name'], PATHINFO_EXTENSION);
	 * $image = Image::createFromFile($_FILES['upfile']['tmp_name'], $ext);
	 * ?>
	 * </code>
	 *
	 * @param string	图像文件的完整路径
	 * @param string	指定扩展名
	 *
	 * @return ImageGD 从文件创建的 ImageGD 对象
	 * @throw Exception
	 */
	static function createFromFile($filename, $fileext = '') {
		if(empty($fileext)) {
			$fileext = self::getFileExtension($filename);
		}
		$fileext = trim(strtolower($fileext), '.');
		$ext2functions = array(
			'jpg'  => 'imagecreatefromjpeg',
			'jpeg' => 'imagecreatefromjpeg',
			'png'  => 'imagecreatefrompng',
			'gif'  => 'imagecreatefromgif'
		);

		if (!isset($ext2functions[$fileext])) {
			throw new Exception('imagecreateform' . $fileext);
		}

		$handle = call_user_func($ext2functions[$fileext], $filename);
		return new ImageGD($handle);
	}

	static function getFileExtension($file){
		return pathinfo($file ,PATHINFO_EXTENSION);
	}

	/**
	 * 将 16 进制颜色值转换为 rgb 值
	 *
	 * 用法：
	 * <code>
	 * <?php
	 * $color = '#369';
	 * list($r, $g, $b) = Image::hex2rgb($color);
	 * echo "red: {$r}, green: {$g}, blue: {$b}";
	 * ?>
	 * </code>
	 *
	 * @param string $color 颜色值
	 * @param string $default 使用无效颜色值时返回的默认颜色
	 *
	 * @return array 由 RGB 三色组成的数组
	 */
	static function hex2rgb($color, $default = 'ffffff') {
		$hex = trim($color, '#&Hh');
		$len = strlen($hex);
		if ($len == 3)
		{
			$hex = "{$hex[0]}{$hex[0]}{$hex[1]}{$hex[1]}{$hex[2]}{$hex[2]}";
		}
		elseif ($len < 6)
		{
			$hex = $default;
		}
		$dec = hexdec($hex);
		return array(($dec >> 16) & 0xff, ($dec >> 8) & 0xff, $dec & 0xff);
	}
}

/**
 * ImageGD 类封装了一个 gd 句柄，用于对图像进行操作
 *
 * @author tomqin <tomqin@gmail.com>
 * @copyright Copyright (c) 2009-2012 SupeFun.Com Inc.
 * @package Core
 */
class ImageGD
{
	/**
	 * GD 资源句柄
	 *
	 * @var resource
	 */
	protected $_handle = null;

	/**
	 * 构造函数
	 *
	 * @param resource $handle GD 资源句柄
	 */
	function __construct($handle) {
		$this->_handle = $handle;
	}

	/**
	 * 析构函数
	 */
	function __destruct() {
		$this->destroy();
	}

	function width() {
		return imagesx($this->_handle);
	}

	function height() {
		return imagesy($this->_handle);
	}

	function resource() {
		return $this->_handle;
	}

	/**
	 * 快速缩放图像到指定大小（质量较差）
	 *
	 * @param integer	新的宽度
	 * @param integer	新的高度
	 *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function resize($width, $height) {
		if (is_null($this->_handle)) {
			return $this;
		}

		$dest = imagecreatetruecolor($width, $height);
		imagecopyresized($dest, $this->_handle, 0, 0, 0, 0, $width, $height, imagesx($this->_handle), imagesy($this->_handle));
		imagedestroy($this->_handle);
		$this->_handle = $dest;
		return $this;
	}

	/**
	 * 缩放图像到指定大小（质量较好，速度比 resize() 慢）
	 *
	 * @param integer	新的宽度
	 * @param integer	新的高度
	 *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function resampled($width, $height) {
		if (is_null($this->_handle)) {
			return $this;
		}
		$dest = imagecreatetruecolor($width, $height);
		imagecopyresampled($dest, $this->_handle, 0, 0, 0, 0, $width, $height, imagesx($this->_handle), imagesy($this->_handle));
		imagedestroy($this->_handle);
		$this->_handle = $dest;
		return $this;
	}

	/**
	 * 调整图像大小，但不进行缩放操作
	 *
	 * 用法：
	 * <code>
	 * $image->crop($width, $height);
	 * </code>
	 *
	 * 该方法还可以指定第三个 $options 参数，该参数可用值有：
	 *
	 * - pos (string): 缩放时的对齐方式
	 * - bgcolor (string): 缩放时多余部分的背景色
	 *
	 * 其中 $options['pos'] 参数的可用值有：
	 *
	 * - left: 左对齐
	 * - right: 右对齐
	 * - center: 中心对齐
	 * - top: 顶部对齐
	 * - bottom: 底部对齐
	 * - top-left, left-top: 左上角对齐
	 * - top-right, right-top: 右上角对齐
	 * - bottom-left, left-bottom: 左下角对齐
	 * - bottom-right, right-bottom: 右下角对齐
	 *
	 * 如果指定了无效的 $options['pos'] 参数，则等同于指定 center。
	 *
	 * @param integer	新的高度
	 * @param integer	新的宽度
	 * @param array		裁减选项
	 *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function crop($width, $height, $options = array()) {
		if (is_null($this->_handle)) {
			return $this;
		}
		$default_options = array(
			'pos'	   => 'center',
			'bgcolor'   => '0xfff',
		);
		$options = array_merge($default_options, $options);

		$dest = imagecreatetruecolor($width, $height);
		$sx = imagesx($this->_handle);
		$sy = imagesy($this->_handle);

		// 根据 pos 属性来决定如何定位原始图片
		switch (strtolower($options['pos'])) {
			case 'left':
				$ox = 0;
				$oy = ($height - $sy) / 2;
			break;
			case 'right':
				$ox = $width - $sx;
				$oy = ($height - $sy) / 2;
			break;
			case 'top':
				$ox = ($width - $sx) / 2;
				$oy = 0;
			break;
			case 'bottom':
				$ox = ($width - $sx) / 2;
				$oy = $height - $sy;
			break;
			case 'top-left':
			case 'left-top':
				$ox = $oy = 0;
			break;
			case 'top-right':
			case 'right-top':
				$ox = $width - $sx;
				$oy = 0;
			break;
			case 'bottom-left':
			case 'left-bottom':
				$ox = 0;
				$oy = $height - $sy;
			break;
			case 'bottom-right':
			case 'right-bottom':
				$ox = $width - $sx;
				$oy = $height - $sy;
			break;
			case 'center':
			default:
				$ox = ($width - $sx) / 2;
				$oy = ($height - $sy) / 2;
		}

		list ($r, $g, $b) = Image::hex2rgb($options['bgcolor'], '0xffffff');
		$bgcolor = imagecolorallocate($dest, $r, $g, $b);
		imagefilledrectangle($dest, 0, 0, $width, $height, $bgcolor);
		imagecolordeallocate($dest, $bgcolor);

		imagecopy($dest, $this->_handle, $ox, $oy, 0, 0, $sx, $sy);
		imagedestroy($this->_handle);
		$this->_handle = $dest;

		return $this;
	}

	/**
	 * 在保持图像长宽比的情况下将图像裁减到指定大小
	 *
	 * thumb() 在缩放图像时，可以保持图像的长宽比，从而保证图像不会拉高或压扁。
	 *
	 * thumb() 默认情况下会按照 $width 和 $height 参数计算出最大缩放比例，
	 * 保持裁减后的图像能够最大程度的充满图片。
	 *
	 * 例如源图的大小是 800 x 600，而指定的 $width 和 $height 是 200 和 100。
	 * 那么源图会被首先缩小为 200 x 150 尺寸，然后裁减掉多余的 50 像素高度。
	 *
	 * 用法：
	 * <code>
	 * $image->thumb($width, $height);
	 * </code>
	 *
	 * 如果希望最终生成图片始终包含完整图像内容，那么应该指定 $options 参数。
	 * 该参数可用值有：
	 *
	 * - fullimage (boolean): 是否保持完整图像
	 * - pos (string): 缩放时的对齐方式
	 * - bgcolor (string): 缩放时多余部分的背景色
	 * - enlarge (boolean): 是否允许放大
	 * - reduce (boolean): 是否允许缩小
	 *
	 * 其中 $options['pos'] 参数的可用值有：
	 *
	 * - left: 左对齐
	 * - right: 右对齐
	 * - center: 中心对齐
	 * - top: 顶部对齐
	 * - bottom: 底部对齐
	 * - top-left, left-top: 左上角对齐
	 * - top-right, right-top: 右上角对齐
	 * - bottom-left, left-bottom: 左下角对齐
	 * - bottom-right, right-bottom: 右下角对齐
	 *
	 * 如果指定了无效的 $options['pos'] 参数，则等同于指定 center。
	 *
	 * $options 中的每一个选项都可以单独指定，例如在允许裁减的情况下将图像放到新图片的右下角。
	 *
	 * <code>
	 * $image->thumb($width, $height, array('pos' => 'right-bottom'));
	 * </code>
	 *
	 * @param integer	新的高度
	 * @param integer	新的宽度
	 * @param array		裁减选项
	  *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function thumb($width, $height, $options = array()) {
		if (is_null($this->_handle)) {
			return $this;
		}

		$default_options = array(
			'fullimage' => false,
			'pos'	   => 'center',
			'bgcolor'   => '0xfff',
			'enlarge'   => false,
			'reduce'	=> true,
		);
		$options = array_merge($default_options, $options);

		// 创建目标图像
		$dest = imagecreatetruecolor($width, $height);
		// 填充背景色
		list ($r, $g, $b) = Image::hex2rgb($options['bgcolor'], '0xffffff');
		$bgcolor = imagecolorallocate($dest, $r, $g, $b);
		imagefilledrectangle($dest, 0, 0, $width, $height, $bgcolor);
		imagecolordeallocate($dest, $bgcolor);

		// 根据源图计算长宽比
		$full_w = imagesx($this->_handle);
		$full_h = imagesy($this->_handle);
		$ratio_w = doubleval($width) / doubleval($full_w);
		$ratio_h = doubleval($height) / doubleval($full_h);

		if ($options['fullimage']) {
			// 如果要保持完整图像，则选择最小的比率
			$ratio = $ratio_w < $ratio_h ? $ratio_w : $ratio_h;
		} else {
			// 否则选择最大的比率
			$ratio = $ratio_w > $ratio_h ? $ratio_w : $ratio_h;
		}

		if (!$options['enlarge'] && $ratio > 1) $ratio = 1;
		if (!$options['reduce'] && $ratio < 1) $ratio = 1;

		// 计算目标区域的宽高、位置
		$dst_w = $full_w * $ratio;
		$dst_h = $full_h * $ratio;

		// 根据 pos 属性来决定如何定位
		switch (strtolower($options['pos'])) {
			case 'left':
				$dst_x = 0;
				$dst_y = ($height - $dst_h) / 2;
			break;
			case 'right':
				$dst_x = $width - $dst_w;
				$dst_y = ($height - $dst_h) / 2;
			break;
			case 'top':
				$dst_x = ($width - $dst_w) / 2;
				$dst_y = 0;
			break;
			case 'bottom':
				$dst_x = ($width - $dst_w) / 2;
				$dst_y = $height - $dst_h;
			break;
			case 'top-left':
			case 'left-top':
				$dst_x = $dst_y = 0;
			break;
			case 'top-right':
			case 'right-top':
				$dst_x = $width - $dst_w;
				$dst_y = 0;
			break;
			case 'bottom-left':
			case 'left-bottom':
				$dst_x = 0;
				$dst_y = $height - $dst_h;
			break;
			case 'bottom-right':
			case 'right-bottom':
				$dst_x = $width - $dst_w;
				$dst_y = $height - $dst_h;
			break;
			case 'center':
			default:
				$dst_x = ($width - $dst_w) / 2;
				$dst_y = ($height - $dst_h) / 2;
		}

		imagecopyresampled($dest,  $this->_handle, $dst_x, $dst_y, 0, 0, $dst_w, $dst_h, $full_w, $full_h);
		imagedestroy($this->_handle);
		$this->_handle = $dest;

		return $this;
	}


	/**
	 * 设置水印
	 *
	 * 用法：
	 * <code>
	 * $image->waterMark($image, array('pos'=>'left-bottom'));
	 * </code>
	 *
	 * 如果希望最终生成图片始终包含完整图像内容，那么应该指定 $options 参数。
	 * 该参数可用值有：
	 *
	 * - pos (string): 缩放时的对齐方式
	 * - bgcolor (string): 缩放时多余部分的背景色
	 * - quality (integer): 水印质量
	 *
	 * 其中 $options['pos'] 参数的可用值有：
	 *
	 * - left: 左对齐
	 * - right: 右对齐
	 * - center: 中心对齐
	 * - top: 顶部对齐
	 * - bottom: 底部对齐
	 * - top-left, left-top: 左上角对齐
	 * - top-right, right-top: 右上角对齐
	 * - bottom-left, left-bottom: 左下角对齐
	 * - bottom-right, right-bottom: 右下角对齐
	 *
	 * 如果指定了无效的 $options['pos'] 参数，则等同于指定 center。
	 *
	 * @param ImageGD	水印图片
	 * @param array		水印选项
	  *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function waterMark($image, $options = array()) {
		if (is_null($this->_handle)) {
			return $this;
		}

		$default_options = array(
			'pos'	   => 'center',
			'bgcolor'   => '0xfff',
			'quality'	=> 60,
		);
		$options = array_merge($default_options, $options);

		// 根据源图计算长宽比
		$width  = $this->width();
		$height = $this->height();

		// 创建水印对象
		$sx = $image->width();
		$sy = $image->height();

		// 根据 pos 属性来决定如何定位
		switch (strtolower($options['pos'])) {
			case 'left':
				$ox = 0;
				$oy = ($height - $sy) / 2;
			break;
			case 'right':
				$ox = $width - $sx;
				$oy = ($height - $sy) / 2;
			break;
			case 'top':
				$ox = ($width - $sx) / 2;
				$oy = 0;
			break;
			case 'bottom':
				$ox = ($width - $sx) / 2;
				$oy = $height - $sy;
			break;
			case 'top-left':
			case 'left-top':
				$ox = $oy = 0;
			break;
			case 'top-right':
			case 'right-top':
				$ox = $width - $sx;
				$oy = 0;
			break;
			case 'bottom-left':
			case 'left-bottom':
				$ox = 0;
				$oy = $height - $sy;
			break;
			case 'bottom-right':
			case 'right-bottom':
				$ox = $width - $sx;
				$oy = $height - $sy;
			break;
			case 'center':
			default:
				$ox = ($width - $sx) / 2;
				$oy = ($height - $sy) / 2;
		}

		//合成水印
		imagecopymerge($this->_handle, $image->resource(), $ox, $oy, 0, 0, $sx, $sy, $options['quality']);
		$image->destroy();

		return $this;
	}


	/**
	 * 保存为 JPEG 文件
	 *
	 * @param string $filename 保存文件名
	 * @param int $quality 品质参数，默认为 80
	 *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function saveAsJpeg($filename, $quality = 80) {
		imagejpeg($this->_handle, $filename, $quality);
	}

	/**
	 * 保存为 PNG 文件
	 *
	 * @param string $filename 保存文件名
	 *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function saveAsPng($filename) {
		imagepng($this->_handle, $filename);
	}

	/**
	 * 保存为 GIF 文件
	 *
	 * @param string $filename 保存文件名
	 *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function saveAsGif($filename) {
		imagegif($this->_handle, $filename);
	}

	/**
	 * 销毁内存中的图像
	 *
	 * @return ImageGD 返回 ImageGD 对象本身，实现连贯接口
	 */
	function destroy() {
		if (!$this->_handle) {
			@imagedestroy($this->_handle);
		}
		$this->_handle = null;
		return $this;
	}
}

